package com.cmes.oauth.core.service;

import cn.dev33.satoken.stp.SaLoginConfig;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpInterface;
import com.cmes.cache.service.CacheService;
import com.cmes.core.context.security.SecurityContextHolder;
import com.cmes.core.utils.AsyncUtils;
import com.cmes.core.utils.CollectionUtils;
import com.cmes.core.utils.Preconditions;
import com.cmes.core.utils.StringUtils;
import com.cmes.oauth.core.constant.ErrorCode;
import com.cmes.security.StpAdminUtil;
import com.cmes.system.core.constant.PermissionTypeEnum;
import com.cmes.system.core.entity.PermissionEntity;
import com.cmes.system.core.entity.RoleEntity;
import com.cmes.system.core.entity.UserEntity;
import com.cmes.system.core.service.LoginLogService;
import com.cmes.system.core.service.PermissionService;
import com.cmes.system.core.service.RoleService;
import com.cmes.system.core.service.UserService;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 登录模块
 *
 * @author Bruce.Gong
 * @since 1.0.0-SNAPSHOT
 */
@Slf4j
@RequiredArgsConstructor
@Service
@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
public class LoginService implements StpInterface {

    private final UserService userService;
    private final RoleService roleService;
    private final PermissionService permissionService;
    private final CacheService cacheService;
    private final LoginLogService loginLogService;
    private final static String AUTH_PERMISSION_ID = "auth:permission:%d";
    private final static String AUTH_ROLE_ID = "auth:role:%d";

    /**
     * 登录成功，返回 Token
     *
     * @param username 用户名
     * @param password 密码
     * @return token
     */
    public String doLogin(String username, String password) {
        try {
            UserEntity db = userService.loginByUsername(username);
            Preconditions.isNull(db, ErrorCode.LOGIN_FAIL);
            String encodePassword = DigestUtils.sha256Hex(password + db.getSalt());
            Preconditions.check(() -> !Objects.equals(db.getPassword(), encodePassword), ErrorCode.LOGIN_FAIL);
            StpAdminUtil.login(db.getId(), loginExtraData(db));
            AsyncUtils.run(() -> loginLogService.success(db.getId(), username, StpAdminUtil.getLoginType()));
            return StpAdminUtil.getTokenValue();
        } catch (Exception e) {
            AsyncUtils.run(() -> loginLogService.fail(username, StpAdminUtil.getLoginType(), e.getMessage()));
            throw e;
        }
    }

    public void logout(Long userId) {
        StpAdminUtil.logout();
        cacheService.removeCache(String.format(AUTH_ROLE_ID, userId));
        cacheService.readCache(String.format(AUTH_PERMISSION_ID, userId));
    }

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        Long userId = Long.parseLong(loginId.toString());
        Set<Long> roleIds = authRoleIds(userId);
        if (CollectionUtils.isEmpty(roleIds)) {
            return Collections.emptyList();
        }
        List<RoleEntity> roles = roleService.listActiveRoles();
        Set<Long> activeRoleIds = roles.stream()
                .map(RoleEntity::getId)
                .filter(roleIds::contains)
                .collect(Collectors.toSet());
        // 系统管理员
        if (activeRoleIds.contains(1L)) {
            return Collections.singletonList("*");
        }
        Set<Long> permissionIds = authPermissionIds(userId, activeRoleIds);
        List<PermissionEntity> permissions = permissionService.listActivePermissions();
        return permissions.stream()
                .filter(v -> permissionIds.contains(v.getId()))
                .map(PermissionEntity::getPerm)
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toList());
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        Long userId = Long.parseLong(loginId.toString());
        Set<Long> roleIds = authRoleIds(userId);
        if (CollectionUtils.isEmpty(roleIds)) {
            return Collections.emptyList();
        }
        List<RoleEntity> roles = roleService.listActiveRoles();
        return roles.stream()
                .filter(v -> roleIds.contains(v.getId()))
                .map(RoleEntity::getCode)
                .collect(Collectors.toList());
    }

    public Set<Long> authRoleIds(Long userId) {
        return cacheService.getCache(String.format(AUTH_ROLE_ID, userId),
                () -> userService.authRoleIds(userId));
    }

    public Set<Long> authPermissionIds(Long userId, Set<Long> roleIds) {
        return cacheService.getCache(String.format(AUTH_PERMISSION_ID, userId),
                () -> roleService.authPermissionIds(roleIds));
    }

    private SaLoginModel loginExtraData(UserEntity user) {
        Map<String, Object> map = Maps.newHashMapWithExpectedSize(4);
        map.put("username", user.getUsername());
        map.put("nickname", user.getNickname());
        map.put("gender", user.getGender());
        map.put("avatar", user.getAvatar());
        return SaLoginConfig.setExtraData(map);
    }

    public Map<String, Object> loginInfo(Long userId) {
        Map<String, Object> result = Maps.newHashMapWithExpectedSize(3);
        UserEntity user = new UserEntity();
        user.setId(userId);
        user.setUsername(SecurityContextHolder.getCurrentUserName());
        user.setNickname((String) StpAdminUtil.getExtra("nickname"));
        user.setAvatar((String) StpAdminUtil.getExtra("avatar"));
        user.setGender((Integer) StpAdminUtil.getExtra("gender"));
        result.put("user", user);

        Set<Long> roleIds = authRoleIds(userId);
        if (CollectionUtils.isEmpty(roleIds)) {
            result.put("permissions", Collections.emptyList());
            result.put("menus", Collections.emptyList());
            return result;
        }
        List<RoleEntity> roles = roleService.listActiveRoles();
        Set<Long> activeRoleIds = roles.stream()
                .map(RoleEntity::getId)
                .filter(roleIds::contains)
                .collect(Collectors.toSet());
        if (CollectionUtils.isEmpty(activeRoleIds)) {
            result.put("permissions", Collections.emptyList());
            result.put("menus", Collections.emptyList());
            return result;
        }
        if (activeRoleIds.contains(1L)) {
            result.put("permissions", Collections.singletonList("*"));
            result.put("menus", CollectionUtils.listTree(permissionService.listActivePermissions(),
                    0L,
                    PermissionEntity::getId,
                    PermissionEntity::getPid,
                    PermissionEntity::setChildren,
                    v -> PermissionTypeEnum.MENU.match(v.getType()),
                    Comparator.comparing(PermissionEntity::getPid).thenComparing(PermissionEntity::getSort)));
            return result;
        }
        Set<Long> permissionIds = roleService.authPermissionIds(activeRoleIds);
        List<PermissionEntity> permissions = permissionService.listActivePermissions();
        result.put("permissions", permissions.stream()
                .filter(v -> permissionIds.contains(v.getId()))
                .map(PermissionEntity::getPerm)
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toList()));
        result.put("menus", CollectionUtils.listTree(permissions,
                0L,
                PermissionEntity::getId,
                PermissionEntity::getPid,
                PermissionEntity::setChildren,
                v -> PermissionTypeEnum.MENU.match(v.getType()) && permissionIds.contains(v.getId()),
                Comparator.comparing(PermissionEntity::getPid).thenComparing(PermissionEntity::getSort)));
        return result;
    }

}
